/*                                      _
 *     ___ _ __ ___ _ __ ___   ___   __| |
 *    / __| '__/ __| '_ ` _ \ / _ \ / _` |
 *    \__ \ |  \__ \ | | | | | (_) | (_| |
 *    |___/_|  |___/_| |_| |_|\___/ \__,_|                                  
 *
 *  _| _  _ _  _    _   . _  |` _  __|_ _  _|
 * (_|(/__\|_)(_|VV| |  || |~|~(/_(_ | (/_(_|                               
 *         |     _ _  _  _|   | _  
 *              | | |(_)(_||_||(/_
 *     
 ******************************************
 ******************************************
 ******************************************
 * . _ _  _ | _  _ _  _  _ _|_ _ _|_. _  _ 
 * || | ||_)|(/_| | |(/_| | | (_| | |(_)| |
 *       |                                 
 *		Using Disassembly and the Linux binary files, we investigated
 *		how L4D keeps track of Survivor Advancement in a Map. It's called Flow 
 *		and stands for any Entities linear Progress on the "Path" from Starting
 *		to Ending Saferoom, in ingame float units. We call the native game
 *		functions to retrieve both Players and Infected Flow value.
 *		
 *		From there it's a matter of comparing them, and, since the Infected
 *		sometimes spawn very far away during events, adding some extra checks
 *		which require the Survivors to advance before removing Zombies occurs.
 *
 *		Sometimes the flow is negative (e.g. infected-only area), so we check 
 *		for that also. Furthermore in order to prevent aggressive despawning 
 *		(e.g. during crescendos) we do a minimum lifetime check on the zombies
 *		to make sure they've been spawned for at least a few seconds (which 
 *		gives them a chance to run to the survivors if they just spawned).
 *	
 *		To make sure the Plugin does not remove Zombies in plain Sight it runs
 *		Ray Traces from all Survivors to a leftbehind Zombie and checks for
 *		them being disrupted by something. Also, the Respawner stops working
 *		as the Survivors approach a Saferoom.
 */

#define MODULE_NAME "DespawnInfected"

static const String:GAMECONFIG_FILE[]				= "srsmod";
static const String:GAMECONFIG_INFECTED_FLOW[]		= "Infected_GetFlowDistance";
static const String:GAMECONFIG_PLAYER_FLOW[]		= "CTerrorPlayer_GetFlowDistance";

static const String:CLASSNAME_INFECTED[]			= "infected";
static const String:CLASSNAME_WITCH[]				= "witch";
static const String:CLASSNAME_PHYSPROPS[]			= "prop_physics";

//why not static const? cause pawn is cool and wont let you use consts to initialize other consts
#define				L4D_MAX_ENTITIES				  2048

static const 		FLOWTYPE_DEFAULT 				= 0;

static const Float:	TRACE_TOLERANCE 				= 75.0;
static const Float:	ZOMBIE_CHECK_INTERVAL 			= 1.5;
static const Float:	ZOMBIE_RESPAWN_INTERVAL 		= 0.5;

static Handle:fGetInfFlowDist 						= INVALID_HANDLE;
static Handle:fPlayerGetFlowDistance 				= INVALID_HANDLE;

static Handle:cvarDespawnInfected 					= INVALID_HANDLE;
static Handle:cvarDespawnDistance 					= INVALID_HANDLE;
static Handle:cvarDespawnNeededAdvance				= INVALID_HANDLE;
static Handle:cvarDespawnNeededLifetime				= INVALID_HANDLE;
static Handle:cvarRespawnRemovedCI 					= INVALID_HANDLE;
static Handle:cvarSafeRoomNear						= INVALID_HANDLE;

static infectedInSpawnQueue							= 0;
static Float:lastLowestSurvivorFlow					= 0.0;

static Float:zombieLifetimes[L4D_MAX_ENTITIES+1]	= 0.0;

static bool:despawnerEnabled						= false;
static bool:eventsHooked							= false;

public DespawnInfected_OnModuleLoaded()
{
	_DI_PrepareSDKCalls();

	cvarDespawnInfected = 		CreateConVar("srs_infected_despawn", 				"1", 		" Enable or Disable the Infected Despawner ", 																			SRS_CVAR_DEFAULT_FLAGS);
	cvarDespawnDistance = 		CreateConVar("srs_infected_despawn_distance", 		"700.0", 	" How far behind a Zombie has to be for removal, in Ingame Distance Units per Second ", 								SRS_CVAR_DEFAULT_FLAGS);
	cvarDespawnNeededAdvance = 	CreateConVar("srs_infected_despawn_min_advance", 	"33.0", 	" How much distance in Ingame Distance Units per Second the Survivors must have advanced for Despawning to trigger ", 	SRS_CVAR_DEFAULT_FLAGS);
	cvarDespawnNeededLifetime = CreateConVar("srs_infected_despawn_min_lifetime", 	"15.0", 	" How many seconds a Zombie should be alive for before it can be despawned", 											SRS_CVAR_DEFAULT_FLAGS);
	cvarSafeRoomNear = 			CreateConVar("srs_infected_despawn_near_safety", 	"1000.0", 	" If the Survivors are this close to the Saferoom the Despawner stops working ", 										SRS_CVAR_DEFAULT_FLAGS);
	cvarRespawnRemovedCI = 		CreateConVar("srs_infected_respawn", 				"1", 		" Enable or Disable respawning of de-spawned Common Infected ", 														SRS_CVAR_DEFAULT_FLAGS);
	
	_DI_OnModuleEnabled(); // default ON
	HookConVarChange(cvarDespawnInfected, _DI_FlipActivation);
}

public _DI_FlipActivation(Handle:convar, const String:oldValue[], const String:newValue[])
{
	if (ACTIVATION_FLIP_OFF_TO_ON)
	{
		_DI_OnModuleEnabled();
	}
	else if (ACTIVATION_FLIP_ON_TO_OFF)
	{
		_DI_OnModuleDisabled();
	}
}

static _DI_OnModuleEnabled()
{
	CreateTimer(ZOMBIE_CHECK_INTERVAL, _DI_Check_Timer, _, TIMER_REPEAT); // this is our Zombie Checking Call
	despawnerEnabled = true;

	Debug_Print("Infected Despawner: MaxEntities = %d", GetMaxEntities());
	
	if (GetMaxEntities() > L4D_MAX_ENTITIES)
	{
		ThrowError("Too many entities for this plugin to handle %d -- please recompile plugin with new L4D_MAX_ENTITIES", GetMaxEntities());
	}
	
	HookEvent("round_start", _DI_RoundStart_Event, EventHookMode_PostNoCopy);
	eventsHooked = true;
}

static _DI_OnModuleDisabled()
{
	if (!IsPluginEnding() && eventsHooked)
	{
		UnhookEvent("round_start", _DI_RoundStart_Event, EventHookMode_PostNoCopy);
		eventsHooked = false;
	}
	
	despawnerEnabled = false;
}

public _DI_OnEntityCreated(entity, const String:classname[])
{
	if (StrEqual(classname, CLASSNAME_INFECTED, .caseSensitive = false))
	{
		zombieLifetimes[entity] = GetGameTime();
	}
}

public _DI_OnEntityDestroyed(entity)
{
	if (entity > 0 && entity < L4D_MAX_ENTITIES)
	{
		zombieLifetimes[entity] = 0.0;
	}
}

public Action:_DI_RoundStart_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	new maxEnts = GetMaxEntities();
	for (new i = CLIENT_VALID_LAST+1; i <= maxEnts; i++)
	{
		zombieLifetimes[i] = 0.0;
	}
}

static _DI_PrepareSDKCalls()
{
	new Handle:conf = LoadGameConfigFile(GAMECONFIG_FILE);
	
	if (conf != INVALID_HANDLE)
	{
		Debug_Print("%s.txt Gamedata file loaded", GAMECONFIG_FILE);
	}
	else
	{
		ThrowError("[SRSMOD] Failed to load %s.txt in gamedata folder", GAMECONFIG_FILE);
	}
	
	StartPrepSDKCall(SDKCall_Entity);
	Debug_Print("GetInfectedFlowDistance Call prepped");
	
	new bool:bGetInfFlowDistFuncLoaded = PrepSDKCall_SetFromConf(conf, SDKConf_Signature, GAMECONFIG_INFECTED_FLOW);
	if (!bGetInfFlowDistFuncLoaded)
	{
		ThrowError("[SRSMOD] Could not load the GetInfectedFlowDistance signature");
	}
	
	PrepSDKCall_SetReturnInfo(SDKType_Float, SDKPass_Plain);
	Debug_Print("GetInfectedFlowDistance Signature prepped");
	fGetInfFlowDist = EndPrepSDKCall();
	
	if (fGetInfFlowDist == INVALID_HANDLE)
	{
		ThrowError("[SRSMOD] Could not prep the GetInfectedFlowDistance function");	
	}
	
	StartPrepSDKCall(SDKCall_Player);
	Debug_Print("PlayerGetFlowDistance Call prepped");
	
	new bool:bPGetFlowDistFuncLoaded = PrepSDKCall_SetFromConf(conf, SDKConf_Signature, GAMECONFIG_PLAYER_FLOW);
	if (!bPGetFlowDistFuncLoaded)
	{
		ThrowError("[SRSMOD] Could not load the PlayerGetFlowDistance signature");
	}
	
	PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Plain);
	PrepSDKCall_SetReturnInfo(SDKType_Float, SDKPass_Plain);
	Debug_Print("PlayerGetFlowDistance Signature prepped");
	fPlayerGetFlowDistance = EndPrepSDKCall();
	
	if (fPlayerGetFlowDistance == INVALID_HANDLE)
	{
		ThrowError("[SRSMOD] Could not prep the PlayerGetFlowDistance function");
	}
}

// Infected::GetFlowDistance(void)const
static Float:L4D_GetInfectedFlowDistance(entity)
{
	return SDKCall(fGetInfFlowDist, entity);
}

// CTerrorPlayer::GetFlowDistance(TerrorNavArea::FlowType)const
static Float:L4D_GetPlayerFlowDist(client) // integer flowtype does not actually have an effect, 0 or 1 or anything work fine
{
	return SDKCall(fPlayerGetFlowDistance, client, FLOWTYPE_DEFAULT);
}

public Action:_DI_Check_Timer(Handle:timer)
{
	if (!despawnerEnabled) return Plugin_Stop; // Kill Timer if Module was disabled
	
	if (!IsAllowedGameMode()
	|| !survivorCount
	|| !CountInGameHumans())
	{
		return Plugin_Continue;
	}

	new Float:lastSurvivorFlow = 0.0;
	new Float:firstSurvivorFlow = 0.0;
	new Float:checkAgainst = 0.0;
	new bool:foundOne = false;
	new firstSurvivor = 0;
	
	FOR_EACH_ALIVE_SURVIVOR_INDEXED(i)
	{
		checkAgainst = L4D_GetPlayerFlowDist(i);
		
		if (checkAgainst < lastSurvivorFlow || lastSurvivorFlow == 0.0)
		{
			lastSurvivorFlow = checkAgainst;
			foundOne = true;
		}
		
		if (checkAgainst > firstSurvivorFlow || firstSurvivorFlow == 0.0)
		{
			firstSurvivorFlow = checkAgainst;
			firstSurvivor = i;
		}
	}
	
	if (!foundOne) return Plugin_Continue; // no valid Survivor Flow found? abort
	
	TryDespawningCommonZombies(lastSurvivorFlow, firstSurvivor);
	lastLowestSurvivorFlow = lastSurvivorFlow;
	
	return Plugin_Continue;
}

static TryDespawningCommonZombies(Float:lastSurvivorFlow, firstSurvivor)
{
	new Float:despawnDistance = GetConVarFloat(cvarDespawnDistance) * ZOMBIE_CHECK_INTERVAL;
	new Float:requiredAdvance = GetConVarFloat(cvarDespawnNeededAdvance) * ZOMBIE_CHECK_INTERVAL;

	if (lastSurvivorFlow < despawnDistance)
	{
		Debug_PrintSpam("Lowest Flow Survivor (%f) found within (%f) of Map Start Area, aborting", lastSurvivorFlow, despawnDistance);
		return;
	}
	
	decl Float:distanceToSafeRoom;
	if (IsNearSafeRoom(firstSurvivor, /* out */distanceToSafeRoom))
	{
		Debug_PrintSpam("Survivor (%N), Flow (%f) is too close (%f) to the Saferoom, aborting Despawner", firstSurvivor, L4D_GetPlayerFlowDist(firstSurvivor), distanceToSafeRoom);
		return;
	}
	
	new Float:flowDifference = (lastSurvivorFlow - lastLowestSurvivorFlow);
	if (flowDifference < requiredAdvance)
	{
		Debug_PrintSpam("Flow advance of (%f) too low, want (%f) to run Despawning", flowDifference, requiredAdvance);
		return;
	}

	new maxEnts = GetMaxEntities();
	decl String:entClass[128];
	decl Float:zombieFlow;
	
	for (new i = CLIENT_VALID_LAST+1; i <= maxEnts; i++)
	{
		if (!IsValidEntity(i)) continue; // ent validity checkAgainst
		
		GetEdictClassname(i, entClass, sizeof(entClass));
		if (!StrEqual(entClass, CLASSNAME_INFECTED, .caseSensitive = false)) continue; // and BAM it's a zombie
		
		zombieFlow = L4D_GetInfectedFlowDistance(i);
		
		if (zombieFlow < 0) //zombies in infected-only areas don't have flow
		{
			Debug_PrintSpam("Skipping Zombie %i, Flow (%f) is in an infected only area", i, zombieFlow);
			continue;
		}
		
		new Float:changeInLifetime = GetGameTime() - zombieLifetimes[i];
		if (changeInLifetime < GetConVarFloat(cvarDespawnNeededLifetime))
		{
			Debug_PrintSpam("Lifetime for Zombie %i, Flow (%f) is not enough (%f secs)", i, zombieFlow, changeInLifetime);
			continue;
		}
		
		if (lastSurvivorFlow - despawnDistance > zombieFlow) // if the Zombie is left far behind
		{
			if (!IsVisibleToSurvivors(i))
			{
				RemoveEdict(i);
				Debug_Print("Removed Zombie %i, Flow (%f) for being way behind (Last Survivor :%f) and invisible", i, zombieFlow, lastSurvivorFlow);
				if (GetConVarBool(cvarRespawnRemovedCI))
				{
					if (infectedInSpawnQueue < 1)
					{
						CreateTimer(ZOMBIE_RESPAWN_INTERVAL, _DI_RespawnInfected_Timer, _, TIMER_REPEAT);
					}
					
					infectedInSpawnQueue++;
				}
			}
			else
			{
				Debug_PrintSpam("Found Zombie %i way behind but visible, Flow (%f), Last Survivorflow (%f)", i, zombieFlow, lastSurvivorFlow);
			}			
		}
	}
}

static bool:IsNearSafeRoom(firstSurvivor, &Float:distance = 0.0)
{
	decl Float:safeRoomPosition[3];
	if (GetSafeRoomPosition(safeRoomPosition)) // since this returns false on Finale Maps were covered
	{
		decl Float:firstSurvivorPosition[3];
		GetClientAbsOrigin(firstSurvivor, firstSurvivorPosition);
		
		distance = GetVectorDistance(firstSurvivorPosition, safeRoomPosition);
		
		if (distance < GetConVarFloat(cvarSafeRoomNear))
		{
			return true;
		}
	}
	
	return false;
}

public Action:_DI_RespawnInfected_Timer(Handle:timer) // respawn Infected with pauses, z_spawn does not spawn 10 things at once
{
	if (infectedInSpawnQueue < 1)
	{
		return Plugin_Stop; // only work if there is a respawn needed, kill timer if not
	}
	
	CheatCommand(_, "z_spawn", "infected auto");
	Debug_Print("One Zombie was respawned, %i remain in queue", infectedInSpawnQueue);
	infectedInSpawnQueue--;
	
	return Plugin_Continue;
}

static bool:IsVisibleToSurvivors(entity) // loops alive Survivors and checks entity for being visible
{
	FOR_EACH_ALIVE_SURVIVOR_INDEXED(i)
	{
		if (IsVisibleTo(i, entity)) 
		{
			return true;
		}
	}
	
	return false;
}

static bool:IsVisibleTo(client, entity) // check an entity for being visible to a client
{
	decl Float:vAngles[3], Float:vOrigin[3], Float:vEnt[3], Float:vLookAt[3];
	
	GetClientEyePosition(client,vOrigin); // get both player and zombie position
	GetEntityAbsOrigin(entity, vEnt);
	
	MakeVectorFromPoints(vOrigin, vEnt, vLookAt); // compute vector from player to zombie
	
	GetVectorAngles(vLookAt, vAngles); // get angles from vector for trace
	
	// execute Trace
	new Handle:trace = TR_TraceRayFilterEx(vOrigin, vAngles, MASK_SHOT, RayType_Infinite, _DI_TraceFilter);
	
	new bool:isVisible = false;
	if (TR_DidHit(trace))
	{
		decl Float:vStart[3];
		TR_GetEndPosition(vStart, trace); // retrieve our trace endpoint
		
		if ((GetVectorDistance(vOrigin, vStart, false) + TRACE_TOLERANCE) >= GetVectorDistance(vOrigin, vEnt))
		{
			isVisible = true; // if trace ray lenght plus tolerance equal or bigger absolute distance, you hit the targeted zombie
		}
	}
	else
	{
		Debug_Print("Zombie Despawner Bug: Player-Zombie Trace did not hit anything, WTF");
		isVisible = true;
	}
	CloseHandle(trace);
	return isVisible;
}

public bool:_DI_TraceFilter(entity, contentsMask)
{
	if (entity <= CLIENT_VALID_LAST || !IsValidEntity(entity)) // dont let WORLD, players, or invalid entities be hit
	{
		return false;
	}
	
	decl String:class[128];
	GetEdictClassname(entity, class, sizeof(class)); // also not zombies or witches, as unlikely that may be, or physobjects (= windows)
	if (StrEqual(class, CLASSNAME_INFECTED, .caseSensitive = false)
	|| StrEqual(class, CLASSNAME_WITCH, .caseSensitive = false)
	|| StrEqual(class, CLASSNAME_PHYSPROPS, .caseSensitive = false))
	{
		return false;
	}
	
	return true;
}

#undef MODULE_NAME
